package dd.lo.practice;

import dd.lo.util.ImageUtils;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import org.apache.commons.lang3.math.NumberUtils;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

import java.io.ByteArrayInputStream;
import java.io.File;

public class HoughLinesDisplay extends Application {

    public static void start(String[] args) {
        launch(args);
    }

    private static final int DEFAULT_WIDTH = 1200;
    private static final int DEFAULT_HEIGHT = 900;

    private static final double RADIAN = Math.PI / 180;

    private static enum DetectMethodEnum {
        HOUGH_LINES("检测直线"),
        HOUGH_LINES_P("检测直线P"),
        ;

        private final String name;

        public String getName() {
            return name;
        }

        DetectMethodEnum(String name) {
            this.name = name;
        }
    }

    private DetectMethodEnum mode;

    private Mat srcImg;

    private SimpleStringProperty rho; //以像素为单位的距离精度，是直线搜索时步进尺寸的单位半径
    private SimpleStringProperty theta; //以弧度为单位的角度精度，是直线搜索时步进尺寸的单位角度
    private SimpleStringProperty threshold; //累加平面的阈值参数，即识别某部分为直线时它在累加平面中必须达到的值
    private SimpleStringProperty srn; //rho的除数距离，用于微调rho的步进速度，精确的累加器步进尺寸为rho/srn
    private SimpleStringProperty stn; //theta的除数距离，用于微调theta的步进速度，精确的累加器步进角度为theta/stn
    private SimpleStringProperty minLineLen; //线段最短长度，小于这个值的线段会被过滤掉
    private SimpleStringProperty maxLineGap; //允许将同一行点与点之间连接起来的最大距离，即线与线之间最大的空隙

    private Slider rhoSlider; //直线搜索步进半径
    private Slider thetaSlider; //直线搜索步进角度
    private Slider thresholdSlider; //累加平面阈值
    private Slider srnSlider; //Sobel算子的X方向的差分阶数
    private Slider stnSlider; //Sobel算子的Y方向的差分阶数
    private Slider minLineLenSlider; //Laplacian算子、Scharr滤波器的缩放因子
    private Slider maxLineGapSlider; //Laplacian算子、Scharr滤波器的缩放因子
    private ImageView imageView;

    private void imageProcess() {
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() {
                if (null == srcImg || srcImg.empty()) return null;
                Mat displayImg = new Mat();
                int rhoV = NumberUtils.toInt(rho.getValue(), 1);
                double thetaV = NumberUtils.toDouble(theta.getValue(), Math.PI / 180.0);
                int thresholdV = NumberUtils.toInt(threshold.getValue(), 1);
                int srnV = NumberUtils.toInt(srn.getValue(), 0);
                int stnV = NumberUtils.toInt(stn.getValue(), 0);
                int minLineLenV = NumberUtils.toInt(minLineLen.getValue(), 0);
                int maxLineGapV = NumberUtils.toInt(maxLineGap.getValue(), 0);
                Mat lines = new Mat(); //lines将存储识别出来的线段信息
                Mat midImg = new Mat(); //中间过渡图
                Imgproc.Canny(srcImg, midImg, 50, 200, 3); //midImg会变成灰度图
                Imgproc.cvtColor(midImg, displayImg, Imgproc.COLOR_GRAY2BGR); //将中间过渡图转到displayImg上时还原为三通道图片
                if (mode == DetectMethodEnum.HOUGH_LINES) {
                    Imgproc.HoughLines(midImg, lines, rhoV, thetaV, thresholdV, srnV, stnV);
                    if (!lines.empty()) { //依次在图中绘制出每条线段
                        int n = (int) lines.size().height;
                        for (int i = 0; i < n; ++i) {
                            double[] element = lines.get(i, 0);
                            double r = element[0], t = element[1];
                            double a = Math.cos(t), b = Math.sin(t);
                            double x0 = a * r, y0 = b * r;
                            Point p1 = new Point(Math.round(x0 + 1000 * -b), Math.round(y0 + 1000 * a));
                            Point p2 = new Point(Math.round(x0 - 1000 * -b), Math.round(y0 - 1000 * a));
                            Imgproc.line(displayImg, p1, p2, new Scalar(50, 100, 195), 2, Imgproc.LINE_AA);
                        }
                    }
                } else if (mode == DetectMethodEnum.HOUGH_LINES_P) {
                    Imgproc.HoughLinesP(midImg, lines, rhoV, thetaV, thresholdV, minLineLenV, maxLineGapV);
                    if (!lines.empty()) { //依次在图中绘制出每条线段
                        int n = (int) lines.size().height;
                        for (int i = 0; i < n; ++i) {
                            double[] point = lines.get(i, 0);
                            Point p1 = new Point(Math.round(point[0]), Math.round(point[1]));
                            Point p2 = new Point(Math.round(point[2]), Math.round(point[3]));
                            Imgproc.line(displayImg, p1, p2, new Scalar(186, 88, 255), 2, Imgproc.LINE_AA);
                        }
                    }
                }
                //显示
                ImageUtils.fitImgSize(displayImg, DEFAULT_WIDTH, DEFAULT_HEIGHT);
                MatOfByte buffer = new MatOfByte();
                Imgcodecs.imencode(".jpeg", displayImg, buffer);
                Platform.runLater(() -> imageView.setImage(new Image(new ByteArrayInputStream(buffer.toArray()))));
                return null;
            }
        };
        new Thread(task).start();
    }

    @Override
    public void start(Stage stage) throws Exception {
        Button imgChooserBtn = new Button("选择图片");
        imgChooserBtn.setOnMouseClicked(e -> {
            FileChooser fileChooser = new FileChooser();
            fileChooser.setTitle("选择图片");
            fileChooser.setInitialDirectory(new File(System.getProperty("user.home") + "/Downloads")
            );
            fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("All Images", "*.*"));
            File imgFile = fileChooser.showOpenDialog(stage);
            if (null == imgFile) return;
            srcImg = Imgcodecs.imread(imgFile.getAbsolutePath(), Imgcodecs.IMREAD_UNCHANGED);
            if (srcImg.empty()) System.out.println("图片读取失败");
            imageProcess();
        });
        Text detectMethodTip = new Text("霍夫变换方法：");
        ChoiceBox<DetectMethodEnum> detectMode = new ChoiceBox<>();
        detectMode.getItems().addAll(DetectMethodEnum.HOUGH_LINES, DetectMethodEnum.HOUGH_LINES_P);
        detectMode.setConverter(new StringConverter<DetectMethodEnum>() {
            @Override
            public String toString(DetectMethodEnum object) {
                return object.getName();
            }
            @Override
            public DetectMethodEnum fromString(String string) {
                return null;
            }
        });
        detectMode.valueProperty().addListener((observable, oldValue, newValue) -> {
            mode = newValue;
            imageProcess();
        });
        detectMode.setValue(DetectMethodEnum.HOUGH_LINES); //初始化值
        //rho和theta的设定
        Text rhoTip = new Text("rho值：");
        rho = new SimpleStringProperty();
        Text rhoText = new Text();
        rhoText.textProperty().bind(rho);
        rhoSlider = new Slider(1, 8, 1);
        rhoSlider.setPrefWidth(DEFAULT_WIDTH * 0.4);
        rhoSlider.setShowTickLabels(true);
        rhoSlider.setShowTickMarks(true);
        rhoSlider.setMajorTickUnit(1.0);
        rhoSlider.setMinorTickCount(0);
        rhoSlider.setBlockIncrement(1.0);
        rhoSlider.valueProperty().addListener((observable, oldValue, newValue) -> {
            rho.set(Math.round(newValue.doubleValue()) + "");
            imageProcess();
        });
        rho.set(Math.round(rhoSlider.getValue()) + ""); //初始化值
        Text thetaTip = new Text("theta值：");
        theta = new SimpleStringProperty();
        Text thetaText = new Text();
        thetaText.textProperty().bind(theta);
        thetaSlider = new Slider(RADIAN, 10 * RADIAN, RADIAN);
        thetaSlider.setPrefWidth(DEFAULT_WIDTH * 0.4);
        thetaSlider.setShowTickLabels(true);
        thetaSlider.setBlockIncrement(RADIAN);
        thetaSlider.valueProperty().addListener((observable, oldValue, newValue) -> {
            theta.set(newValue.doubleValue() + "");
            imageProcess();
        });
        theta.set(thetaSlider.getValue() + ""); //初始化值
        //平面累加阈值的设定
        Text thresholdTip = new Text("平面累积阈值：");
        threshold = new SimpleStringProperty();
        Text thresholdText = new Text();
        thresholdText.textProperty().bind(threshold);
        thresholdSlider = new Slider(1, 255, 150);
        thresholdSlider.setPrefWidth(DEFAULT_WIDTH * 0.4);
        thresholdSlider.setShowTickLabels(true);
        thresholdSlider.setShowTickMarks(true);
        thresholdSlider.setMajorTickUnit(10.0);
        thresholdSlider.setMinorTickCount(1);
        thresholdSlider.setBlockIncrement(1.0);
        thresholdSlider.valueProperty().addListener((observable, oldValue, newValue) -> {
            threshold.set(Math.round(newValue.doubleValue()) + "");
            imageProcess();
        });
        threshold.set(Math.round(thresholdSlider.getValue()) + ""); //初始化值
        //srn、stn的设定
        Text srnTip = new Text("SRN值：");
        srn = new SimpleStringProperty();
        Text srnText = new Text();
        srnText.textProperty().bind(srn);
        srnSlider = new Slider(0, 8, 0);
        srnSlider.setPrefWidth(DEFAULT_WIDTH * 0.2);
        srnSlider.setShowTickLabels(true);
        srnSlider.setShowTickMarks(true);
        srnSlider.setSnapToTicks(true);
        srnSlider.setMajorTickUnit(1.0);
        srnSlider.setMinorTickCount(0);
        srnSlider.setBlockIncrement(1.0);
        srnSlider.valueProperty().addListener((observable, oldValue, newValue) -> {
            srn.set(Math.round(newValue.doubleValue()) + "");
            imageProcess();
        });
        srn.set(Math.round(srnSlider.getValue()) + ""); //初始化值
        Text stnTip = new Text("STN值：");
        stn = new SimpleStringProperty();
        Text stnText = new Text();
        stnText.textProperty().bind(stn);
        stnSlider = new Slider(0, 8, 0);
        stnSlider.setPrefWidth(DEFAULT_WIDTH * 0.2);
        stnSlider.setShowTickLabels(true);
        stnSlider.setShowTickMarks(true);
        stnSlider.setSnapToTicks(true);
        stnSlider.setMajorTickUnit(1.0);
        stnSlider.setMinorTickCount(0);
        stnSlider.setBlockIncrement(1.0);
        stnSlider.valueProperty().addListener((observable, oldValue, newValue) -> {
            stn.set(Math.round(newValue.doubleValue()) + "");
            imageProcess();
        });
        stn.set(Math.round(stnSlider.getValue()) + ""); //初始化值
        //minLineLen、maxLineGap参数设定
        Text minLineLenTip = new Text("线段最短长度参数：");
        minLineLen = new SimpleStringProperty();
        Text minLineLenText = new Text();
        minLineLenText.textProperty().bind(minLineLen);
        minLineLenSlider = new Slider(0, 200, 0);
        minLineLenSlider.setPrefWidth(DEFAULT_WIDTH * 0.4);
        minLineLenSlider.setShowTickLabels(true);
        minLineLenSlider.setShowTickMarks(true);
        minLineLenSlider.setSnapToTicks(true);
        minLineLenSlider.setMajorTickUnit(10.0);
        minLineLenSlider.setMinorTickCount(1);
        minLineLenSlider.setBlockIncrement(1.0);
        minLineLenSlider.valueProperty().addListener((observable, oldValue, newValue) -> {
            minLineLen.set(newValue.toString());
            imageProcess();
        });
        minLineLen.set(Math.round(minLineLenSlider.getValue()) + ""); //初始化值
        Text maxLineGapTip = new Text("线段最短长度参数：");
        maxLineGap = new SimpleStringProperty();
        Text maxLineGapText = new Text();
        maxLineGapText.textProperty().bind(maxLineGap);
        maxLineGapSlider = new Slider(0, 200, 0);
        maxLineGapSlider.setPrefWidth(DEFAULT_WIDTH * 0.4);
        maxLineGapSlider.setShowTickLabels(true);
        maxLineGapSlider.setShowTickMarks(true);
        maxLineGapSlider.setSnapToTicks(true);
        maxLineGapSlider.setMajorTickUnit(10.0);
        maxLineGapSlider.setMinorTickCount(1);
        maxLineGapSlider.setBlockIncrement(1.0);
        maxLineGapSlider.valueProperty().addListener((observable, oldValue, newValue) -> {
            maxLineGap.set(newValue.toString());
            imageProcess();
        });
        maxLineGap.set(Math.round(maxLineGapSlider.getValue()) + ""); //初始化值
        //关闭按钮
        Button exitBtn = new Button("关闭");
        exitBtn.setOnMouseClicked(e -> Platform.exit());
        VBox rows = new VBox(0);
        rows.setPadding(new Insets(10, 10, 0, 10));
        HBox row0 = new HBox(10);
        row0.setPadding(new Insets(5, 0, 5, 0));
        row0.getChildren().addAll(imgChooserBtn, detectMethodTip, detectMode, exitBtn);
        HBox row1 = new HBox(10);
        row1.setPadding(new Insets(5, 0, 5, 0));
        row1.getChildren().addAll(rhoTip, rhoSlider, rhoText, thetaTip, thetaSlider, thetaText);
        HBox row2 = new HBox(10);
        row2.setPadding(new Insets(5, 0, 5, 0));
        row2.getChildren().addAll(srnTip, srnSlider, srnText, stnTip, stnSlider, stnText, thresholdTip, thresholdSlider,thresholdText);
        HBox row3 = new HBox(10);
        row3.setPadding(new Insets(5, 0, 5, 0));
        row3.getChildren().addAll(minLineLenTip, minLineLenSlider, minLineLenText, maxLineGapTip, maxLineGapSlider, maxLineGapText);
        rows.getChildren().addAll(row0, row1, row2, row3);
        BorderPane root = new BorderPane();
        imageView = new ImageView(); //imageView放到一个pane的中间
        root.setCenter(imageView);
        root.setBottom(rows);
        Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT + 550);
        stage.setScene(scene);
        stage.setTitle("霍夫变换直线检测演示（HoughLines/HoughLinesP）");
        stage.setResizable(false);
        stage.show();
    }
}
